Skip to content

Speed up memo navigation: loading skeleton + parallel fetches - #62

Merged
mikaalnaik merged 1 commit into
mainfrom
mikaal/memo-nav-perf
Aug 18, 2026
Merged

Speed up memo navigation: loading skeleton + parallel fetches#62
mikaalnaik merged 1 commit into
mainfrom
mikaal/memo-nav-perf

Conversation

@mikaalnaik

Copy link
Copy Markdown
Contributor

Clicking a memo in production felt unresponsive. Investigating turned up two separate causes.

The click felt dead

The memo route reads cookies (primeAdminPreviewToken + getCurrentUser), so it renders dynamically on every request. With no loading boundary, its prefetch payload was empty:

prefetch RSC on prod → 239 bytes    (nothing usable)
actual RSC on click  → 55,210 bytes

So a click did a full origin round trip while React kept the previous page on screen — no spinner, no skeleton, nothing until the whole memo rendered. Adding loading.tsx makes the skeleton part of the statically prefetchable shell (239 bytes → ~30KB), so a hovered link paints instantly on click.

This is compensation, not a cure — see "Not addressed" below.

The render was needlessly serial

fetchMemo and fetchMemos are independent but ran one after the other, and fetchMemos paged through all 65 memos one request at a time before fetching /team. Now page 1 establishes the page count, the rest go out together, and /team starts up front since it never depended on the pages.

Measured against the live API, 3 runs each, identical output (65 memos, 81 team members):

Runs
Sequential (old) 1987 / 1839 / 1865ms
Parallel (new) 869 / 984 / 909ms ~2.1×, ~950ms saved

This also fixes a latent bug: the pagination loop mutated a single shared queryParams object and set page on it. Safe only because the calls were sequential — parallelizing as-written would have had every request racing on the same key. Each page now builds its own params.

The skeleton

Mirrors the real layout at the same widths and spacing so nothing shifts on swap. Chrome that's identical on every memo is rendered for real — the Key Messages frame, its eyebrow and 01/02/03 numerals, the Signpost's accent rule, track, dots and share buttons — with placeholders only where per-memo content goes. Primitives (.skeleton-bar, -strong, .skeleton-delay-*) live in globals.css for reuse; light grey from the charcoal ramp, with a sweep rather than an opacity pulse. Reduced-motion is already handled globally.

Behaviour change worth a look

Related memos are now non-fatal (fetchMemos().catch(() => [])). Previously a failure there would 500 the entire memo page over a decorative two-item sidebar. This matches how /team failures were already handled in the same file. A bad slug still 404s. Happy to make it fatal again if you'd rather.

Testing

  • tsc --noEmit and eslint clean
  • Dev server: memo renders with correct title and related section; /memos still lists all 65
  • Prefetch payload confirmed to carry the skeleton; skeletonSweep confirmed in compiled CSS
  • Not visually reviewed — no browser available in the session that wrote this. Worth eyeballing on throttled network before merge, particularly the grey bars inside the warm Key Messages box.
  • npm run build fails on /tracker, which prerenders against localhost:3000. Pre-existing and unrelated; the memo routes compile.

Not addressed

Three larger items from the same investigation, in rough order of impact:

  1. The route is dynamic at all. Moving the cookie read out of the page body would make it statically prerendered and CDN-cacheable, turning a prefetched click into a zero-network navigation. Needs a decision on how draft preview should work.
  2. 65 memos fetched to show 2 related ones. Should be a filtered query or streamed behind <Suspense>. This PR made it faster, not smaller.
  3. /team takes ~1.1s upstream and is on nearly every page — now the long pole in the parallel version.

🤖 Generated with Claude Code

Clicking a memo felt unresponsive in production. Two causes:

The memo route reads cookies (draft preview, viewer state), so it renders
dynamically on every request. With no loading boundary its prefetch payload
was empty (239 bytes in prod), so a click did a full round trip with the
previous page still on screen and no feedback — indistinguishable from a
dead click. Adding loading.tsx makes the skeleton statically prefetchable,
so the click paints immediately.

The render itself was also needlessly serial. fetchMemo and fetchMemos are
independent but ran one after the other, and fetchMemos paged through all
65 memos a request at a time before fetching /team. Page 1 now establishes
the page count and the rest go out together, with /team started up front.
Measured against the live API, same output (65 memos, 81 team members):
~1.9s sequential to ~0.9s parallel.

Also fixes a latent bug in the pagination loop, which mutated one shared
queryParams object — safe only while the calls were sequential.

Related memos are now non-fatal: that list backs a decorative two-item
sidebar and shouldn't 500 the whole memo. Matches how /team failures were
already handled here. A bad slug still 404s.

The skeleton renders per-memo content as placeholders but keeps invariant
chrome real — the Key Messages frame, eyebrow and numerals, the Signpost
rail, track and share buttons — so the page reads as a memo mid-load
rather than a generic loading card. Its primitives live in globals.css
for reuse.

This does not address the underlying dynamic rendering, the 65-memo fetch
behind two related memos, or /team taking ~1.1s upstream.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Aug 18, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds prefetched loading skeletons to both memo-detail routes and reduces navigation latency by parallelizing independent memo, pagination, and team requests.

  • Adds a shared memo skeleton with default and Toronto presentation variants.
  • Fetches the requested memo and related-memo list concurrently.
  • Fetches remaining memo pages concurrently while preserving page order and using independent parameter objects.
  • Treats related-memo and team metadata failures as non-fatal.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete changed-code defect identified.

The parallel fetches preserve request parameters and result ordering, token setup completes before concurrent reads begin, and the new non-fatal related-memo behavior is intentional and documented.

Important Files Changed

Filename Overview
src/lib/api/memos.ts Reworks pagination into ordered concurrent page requests, starts team metadata retrieval in parallel, and removes shared query-parameter mutation.
src/app/memos/[slug]/page.tsx Fetches memo detail and the non-critical related-memo collection concurrently while retaining the existing not-found behavior.
src/app/toronto/memos/[slug]/page.tsx Applies the same concurrent detail and related-list retrieval to Toronto-filtered memos.
src/app/memos/[slug]/MemoSkeleton.tsx Introduces the shared accessible loading placeholder used by both memo-detail route variants.
src/app/globals.css Adds reusable skeleton bar styles, sweep animation, emphasis, and stagger classes.
src/app/memos/[slug]/loading.tsx Adds the default memo route loading boundary.
src/app/toronto/memos/[slug]/loading.tsx Adds a Toronto-branded loading boundary with a back-link placeholder.

Sequence Diagram

sequenceDiagram
  participant Route as Memo route
  participant Detail as fetchMemo
  participant List as fetchMemos
  participant API as York Factory API
  Route->>Route: Prime preview token
  par Requested memo
    Route->>Detail: fetchMemo(slug)
    Detail->>API: GET /memos/:slug
  and Related memo list
    Route->>List: fetchMemos(filters)
    par Page count
      List->>API: "GET /memos?page=1"
    and Author metadata
      List->>API: GET /team
    end
    API-->>List: Page 1 + total pages
    par Remaining pages
      List->>API: "GET /memos?page=2"
      List->>API: "GET /memos?page=N"
    end
  end
  Detail-->>Route: Memo or not found
  List-->>Route: Ordered memos or empty related list
Loading

Reviews (1): Last reviewed commit: "Speed up memo navigation: loading skelet..." | Re-trigger Greptile

@mikaalnaik
mikaalnaik merged commit 3bc88ba into main Aug 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant